﻿using System.Buffers;
using System.Threading.Tasks;

namespace System.IO
{
    class StreamToStreamCopy
    {
        public const int DefaultBufferSize = 81920;

        public static async Task<long> CopyAsync(Stream source, Stream destination, long length, int bufferSize = DefaultBufferSize)
        {
            if (!source.CanRead)
                throw new InvalidOperationException("source must be open for reading");
            if (!destination.CanWrite)
                throw new InvalidOperationException("destination must be open for writing");
            if (length < 0)
                throw new ArgumentOutOfRangeException(nameof(length));
            if (length == 0)
                return 0;

            var pool = ArrayPool<byte>.Shared;
            int[] bufl = { 0, 0 };
            byte[][] buf = { pool.Rent(bufferSize), pool.Rent(bufferSize) };
            int bufno = 0;
            Task<int> read = null;
            Task write = null;
            long totalCopyed = 0;

            try
            {
                if (buf[bufno].Length > length)
                    read = source.ReadAsync(buf[bufno], 0, (int)length);
                else
                    read = source.ReadAsync(buf[bufno], 0, buf[bufno].Length);
                
                while (true)
                {
                    await read.ConfigureAwait(false);
                    bufl[bufno] = read.Result;

                    // if zero bytes read, the copy is complete
                    if (bufl[bufno] == 0)
                    {
                        break;
                    }

                    // wait for the in-flight write operation, if one exists, to complete
                    // the only time one won't exist is after the very first read operation completes
                    if (write != null)
                    {
                        await write.ConfigureAwait(false);
                    }

                    // start the new write operation
                    write = destination.WriteAsync(buf[bufno], 0, bufl[bufno]);

                    totalCopyed += bufl[bufno];

                    // toggle the current, in-use buffer
                    // and start the read operation on the new buffer.
                    //
                    // Changed to use XOR to toggle between 0 and 1.
                    // A little speedier than using a ternary expression.
                    bufno ^= 1; // bufno = ( bufno == 0 ? 1 : 0 ) ;

                    if (totalCopyed == length)
                        break;
                    if (totalCopyed + buf[bufno].Length > length)
                        read = source.ReadAsync(buf[bufno], 0, (int)(length - totalCopyed));
                    else
                        read = source.ReadAsync(buf[bufno], 0, buf[bufno].Length);
                }

                // wait for the final in-flight write operation, if one exists, to complete
                // the only time one won't exist is if the input stream is empty.
                if (write != null)
                {
                    await write.ConfigureAwait(false);
                }
            }
            finally
            {
                pool.Return(buf[0]);
                pool.Return(buf[1]);
            }

            await destination.FlushAsync().ConfigureAwait(false);

            // return to the caller ;
            return totalCopyed;
        }
    }
}
